/***
 * Copyright (c) 2021-2031 murenchao
 * fig is licensed under Mulan PubL v2.
 * You can use this software according to the terms and conditions of the Mulan PubL v2.
 * You may obtain a copy of Mulan PubL v2 at:
 *       http://license.coscl.org.cn/MulanPubL-2.0
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PubL v2 for more details.
 */
package cool.taomu.software.fig.asm

import java.io.File
import java.io.IOException
import java.io.PrintWriter
import java.lang.invoke.CallSite
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.util.UUID
import java.util.Vector
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Handle
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.commons.GeneratorAdapter
import org.objectweb.asm.commons.Method
import org.objectweb.asm.util.TraceClassVisitor

class BytecodeUtils {
	var ClassWriter cwriter
	var ClassVisitor tcvisitor
	var String zlassName
	var GeneratorAdapter ga
	var isDebug = false

	interface ICode {
		def void coding(BytecodeUtils _self, GeneratorAdapter adapterx)
	}

	new() {
		this(false)
	}

	new(boolean isDebug) {
		this.isDebug = isDebug
		cwriter = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES)
		if (this.isDebug) {
			tcvisitor = new TraceClassVisitor(cwriter, new PrintWriter(System.err))
		} else {
			tcvisitor = cwriter
		}
	}

	def endClass() {
		tcvisitor.visitEnd()
		var bytes = cwriter.toByteArray()
		if (this.isDebug) {
			try {
				var String uuid = UUID.randomUUID().toString()
				FileUtils.writeByteArrayToFile(new File(String.format("./output_%s.class", uuid)), bytes)
			} catch (IOException e) {
				e.printStackTrace()
			}
		}
		return bytes
	}

	def init(String name, Class<?> superClass, Class<?> superParameterType, String[] interfaces, ICode code) {
		this.zlassName = name;
		tcvisitor.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null, superClass.typeName, interfaces)
		var m = Method.getMethod("void <init>()")
		if (superParameterType !== null) {
			m = Method.getMethod("void <init>(" + Type.getType(superParameterType).internalName + ")", true)
		}
		var ga = new GeneratorAdapter(Opcodes.ACC_PUBLIC, m, null, null, tcvisitor)
		if (code !== null) {
			code.coding(this, ga)
		} else {
			ga.loadThis()
			ga.invokeConstructor(Type.getObjectType(superClass.typeName), m)
		}
		ga.returnValue()
		ga.endMethod()
		return this
	}

	def addPublicMethod(String name, Class<?> ret, Class<?>[] params, ICode code) {
		return addMethod(name, Opcodes.ACC_PUBLIC, ret, params, code)
	}

	def addPrivateMethod(String name, Class<?> ret, Class<?>[] params, ICode code) {
		return addMethod(name, Opcodes.ACC_PRIVATE, ret, params, code)
	}

	def addProtectedMethod(String name, Class<?> ret, Class<?>[] params, ICode code) {
		return addMethod(name, Opcodes.ACC_PROTECTED, ret, params, code)
	}

	def addPublicField(String name, Class<?> type) {
		return addField(name, Opcodes.ACC_PUBLIC, type, null, false)
	}

	def addPrivateField(String name, Class<?> type) {
		return addField(name, Opcodes.ACC_PRIVATE, type, null, true)
	}

	def addProtectedField(String name, Class<?> type, boolean isGetSet) {
		return addField(name, Opcodes.ACC_PROTECTED, type, null, true)
	}

	def addField(String name, int access, Class<?> type, Object value, boolean isGetSet) {
		tcvisitor.visitField(access, name, Type.getType(type).getDescriptor(), null, value).visitEnd();
		if (isGetSet) {
			var fName = "" + name //TODO CommonUtils.camelUpperName(name);
			this.addPublicMethod(#["set", fName].join(), void, #[type], [ a, b |
				b.loadThis();
				b.loadArg(0);
				b.putField(Type.getObjectType(this.zlassName), name, Type.getType(type));
			])
			this.addPublicMethod(#["get", fName].join(), type, null, [ a, b |
				b.loadThis();
				b.getField(Type.getObjectType(this.zlassName), name, Type.getType(type));
			])
		}
		return this
	}

	def addMethod(String name, int access, Class<?> ret, Class<?>[] params, ICode code) {
		var m = createMethod(name, ret, params)
		ga = new GeneratorAdapter(access, m, null, null, tcvisitor)
		if (code !== null) {
			code.coding(this, ga)
		}
		ga.returnValue()
		ga.endMethod()
		return this
	}

	protected def Method createMethod(String name, Class<?> ret, Class<?>[] params) {
		var method = new Vector<String>()
		method.add(ret.typeName)
		method.add(name)
		method.add("(")
		if (params !== null) {
			var ps = new Vector()
			for (p : params) {
				ps.add(Type.getType(p).className.replace(".", "/"))
			}
			if (ps.size > 1) {
				method.add(ps.join(","))
			} else {
				method.addAll(ps)
			}
		}
		method.add(")")
		var methodString = method.join(" ")
		return Method.getMethod(methodString, true)
	}

	def invokeVirtual(String zlassName, String name, Class<?> ret, Class<?>[] params) {
		ga.invokeVirtual(Type.getObjectType(zlassName.replace(".", "/")), createMethod(name, ret, params));
		return this
	}

	def invokeVirtual(Class<?> zlass, String name, Class<?> ret, Class<?>[] params) {
		ga.invokeVirtual(Type.getType(zlass), createMethod(name, ret, params));
		return this
	}

	def invokeInterface(String zlassName, String name, Class<?> ret, Class<?>[] params) {
		ga.invokeInterface(Type.getObjectType(zlassName.replace(".", "/")), createMethod(name, ret, params));
		return this
	}

	def invokeInterface(Class<?> zlass, String name, Class<?> ret, Class<?>[] params) {
		ga.invokeInterface(Type.getType(zlass), createMethod(name, ret, params));
		return this
	}

	def invokeStatic(String zlassName, String name, Class<?> ret, Class<?>[] params) {
		ga.invokeStatic(Type.getObjectType(zlassName.replace(".", "/")), createMethod(name, ret, params));
		return this
	}

	def invokeStatic(Class<?> zlass, String name, Class<?> ret, Class<?>[] params) {
		ga.invokeStatic(Type.getType(zlass), createMethod(name, ret, params));
		return this
	}

	def invokeDynamic(Class<?> zlass,Class<?> ret, Class<?>[] params) {
		var mt = MethodType.methodType(CallSite, MethodHandles.Lookup, String, MethodType);
		var bootstrap = new Handle(Opcodes.H_INVOKESTATIC, Type.getInternalName(zlass), "bootstrap",
			mt.toMethodDescriptorString(), false);
		var m = createMethod("async_call", ret, params)
		ga.invokeDynamic(m.name, m.descriptor, bootstrap);
		return this
	}

	def createInferface(String name, String[] interfaces) {
		tcvisitor.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE, name, null,
			"java/lang/Object", interfaces)
		return this
	}

	def createSerialVersionUID() {
		return addField("serialVersionUID", Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, long, 1L,
			false)
	}

}
